﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CkSoftware.GroupMe.Sdk.Core;
using CkSoftware.GroupMe.Sdk.Core.Dao;
using CkSoftware.GroupMe.Sdk.Core.Exceptions;
using CkSoftware.GroupMe.Sdk.Messaging.Commands;
using CkSoftware.GroupMe.Sdk.Messaging.Dao;
using CkSoftware.GroupMe.Sdk.Messaging.Enums;

namespace CkSoftware.GroupMe.Sdk.Messaging
{
	/// <summary>
	/// BaseClass for message-handling implementations. Provides basic methods for Create, Index, Like and Unlike commands.
	/// </summary>
	public abstract class GroupMeMessages
	{
		private readonly string _conversationId;
		private readonly Dictionary<string, GroupMeMessageData> _loadedMessages;

		private readonly IWebApiAccess _webApiAccess;

		/// <summary>
		/// Defines if the initial-load is completed successfully (and a scrolling can be used).
		/// </summary>
		protected bool InitialLoadDone;

		/// <summary>
		/// Defines the newest loaded message id, used for scrolling.
		/// </summary>
		protected string NewestMessageId;

		/// <summary>
		/// Defines the oldest loaded message id, used for scrolling.
		/// </summary>
		protected string OldestMessageId;

		private LoadMessagesResultData _lastResult;

		/// <summary>
		/// Instantiates a new <see cref="GroupMeMessages" /> object.
		/// </summary>
		/// <param name="webApiAccess">The <see cref="IWebApiAccess" /> to use for API communication.</param>
		/// <param name="conversationId">The conversation id, used for likes and unlikes.</param>
		protected GroupMeMessages(IWebApiAccess webApiAccess, string conversationId)
		{
			_webApiAccess = webApiAccess;
			_conversationId = conversationId;
			_loadedMessages = new Dictionary<string, GroupMeMessageData>();
		}

		/// <summary>
		/// Contains all currently loaded (in-memory) messages, ordered by CreatedAt. Will be cleared if LoadCurrentMessages() is
		/// executed.
		/// </summary>
		public IEnumerable<GroupMeMessageData> LoadedMessages
		{
			get { return _loadedMessages.Values.OrderByDescending(msg => msg.CreatedAt); }
		}

		/// <summary>
		/// Creates a new message in this group.
		/// </summary>
		/// <param name="sourceGuid">
		/// The sourceGuid of this message. This is an identifier of this message and should be unique, to
		/// correctly identify duplicate submits.
		/// </param>
		/// <param name="text">The text to submit (optional if there is an attachment).</param>
		/// <param name="attachments">A list of attachments to add to this mesasge.</param>
		/// <returns>The created message-data.</returns>
		public async Task<GroupMeMessageData> CreateMessage(string sourceGuid, string text = null,
			IEnumerable<GroupMeAttachmentBase> attachments = null)
		{
			if (string.IsNullOrEmpty(sourceGuid))
			{
				throw new ArgumentNullException("sourceGuid");
			}

			IWebApiCommand<GroupMeApiResponse<GroupMeMessageData>> command = GetCreateMessageCommand(sourceGuid, text,
				attachments);

			GroupMeApiResponse<GroupMeMessageData> result = await _webApiAccess.ExecuteCommand(command);

			if (result.Meta.ParsedCode != GroupMeApiResultCode.Created)
			{
				throw new GroupMeException(result.Meta);
			}

			return result.Response;
		}

		/// <summary>
		/// Likes the supplied message.
		/// </summary>
		/// <param name="message">The message to be liked.</param>
		/// <returns>The awaitable task.</returns>
		public async Task LikeMessage(GroupMeMessageData message)
		{
			await LikeMessage(message.MessageId);
		}

		/// <summary>
		/// Likes the message with the supplied message id.
		/// </summary>
		/// <param name="messageId">The messageId of the message to like.</param>
		/// <returns>The awaitable task.</returns>
		public async Task LikeMessage(string messageId)
		{
			var command = new MessageLikeCommand(_conversationId, messageId);
			GroupMeApiResponse<EmptyResponse> result = await _webApiAccess.ExecuteCommand(command);

			if (result.Meta.ParsedCode != GroupMeApiResultCode.Ok)
			{
				throw new GroupMeException(result.Meta);
			}
		}

		/// <summary>
		/// Loads the currently available messages. If there were messages loaded previously (located in LoadedMessages), they will
		/// be cleared.
		/// </summary>
		/// <returns>The result of the message-load.</returns>
		public async Task<LoadMessagesResultData> LoadCurrentMessages()
		{
			LoadMessagesResultData data = await LoadMessages(MessageLoadDirection.Current);
			InitialLoadDone = true;
			return data;
		}

		/// <summary>
		/// Moves max 20 messages backward, from the last known message available.
		/// </summary>
		/// <returns>The message-load result.</returns>
		public async Task<LoadMessagesResultData> ScrollBackward()
		{
			if (!InitialLoadDone)
			{
				return await LoadCurrentMessages();
			}

			return await LoadMessages(MessageLoadDirection.Backward);
		}

		/// <summary>
		/// Moves 20 messages forward, will do a big jump, if there are more than 20 new messages available in this group (will
		/// create a messages-gap).
		/// </summary>
		/// <returns>The message-load result.</returns>
		public async Task<LoadMessagesResultData> ScrollForward()
		{
			if (!InitialLoadDone)
			{
				return await LoadCurrentMessages();
			}

			return await LoadMessages(MessageLoadDirection.Forward);
		}

		/// <summary>
		/// Unlikes the supplied message.
		/// </summary>
		/// <param name="message">The message to be unliked.</param>
		/// <returns>The awaitable task.</returns>
		public async Task UnlikeMessage(GroupMeMessageData message)
		{
			await UnlikeMessage(message.MessageId);
		}

		/// <summary>
		/// Unlikes the message with the supplied message id.
		/// </summary>
		/// <param name="messageId">The messageId of the message to unlike.</param>
		/// <returns>The awaitable task.</returns>
		public async Task UnlikeMessage(string messageId)
		{
			var command = new MessageUnlikeCommand(_conversationId, messageId);
			GroupMeApiResponse<EmptyResponse> result = await _webApiAccess.ExecuteCommand(command);

			if (result.Meta.ParsedCode != GroupMeApiResultCode.Ok)
			{
				throw new GroupMeException(result.Meta);
			}
		}

		/// <summary>
		/// When implemented, delivers a correctly instantiated CreateMessage command.
		/// </summary>
		/// <param name="sourceGuid">The source-guid to use in the command.</param>
		/// <param name="text">The text to use in the command.</param>
		/// <param name="attachments">The attachments to use in the command.</param>
		/// <returns>The correctly constructed IWebApiCommand.</returns>
		protected abstract IWebApiCommand<GroupMeApiResponse<GroupMeMessageData>> GetCreateMessageCommand(string sourceGuid,
			string text, IEnumerable<GroupMeAttachmentBase> attachments);

		/// <summary>
		/// When implemented, delivers a correctly instantiated IndexMessage command for a given direction.
		/// </summary>
		/// <param name="direction">The direction to use in the command.</param>
		/// <returns>The correctly constructed IWebApiCommand.</returns>
		protected abstract IWebApiCommand<GroupMeApiResponse<MessagesIndexData>> GetLoadMessagesCommandForDirection(
			MessageLoadDirection direction);

		private async Task<LoadMessagesResultData> LoadMessages(MessageLoadDirection direction)
		{
			bool isInitial = ResetDataIfDirectionIsCurrent(direction);

			var data = new LoadMessagesResultData();
			IWebApiCommand<GroupMeApiResponse<MessagesIndexData>> command = GetLoadMessagesCommandForDirection(direction);
			GroupMeApiResponse<MessagesIndexData> results = await LoadResultsForCommand(command);

			// Set the current messages count
			if (results.Response != null)
			{
				data.MessagesCount = results.Response.TotalMessagesCount;
			}

			// If there are no messages available, fill out an empty messages IEnumerable and return the data.
			if (results.Meta.ParsedCode == GroupMeApiResultCode.NotModified)
			{
				data.Messages = Enumerable.Empty<GroupMeMessageData>();
				_lastResult = data;
				return data;
			}

			// detect message gap (because of the fact that the sinceId load returns the 20 most-recent messages and there is a possibility that there are more than 20 new messages available.
			if (!isInitial && data.MessagesCount - _lastResult.MessagesCount > 20 && direction == MessageLoadDirection.Forward)
			{
				data.MessagesGapDetected = true;
			}

			// Update oldest and newest message id, according to the used direction.
			if (results.Response != null && results.Response.Messages != null && results.Response.Messages.Any())
			{
				SetMessageIdBoundariesForDirection(direction, results.Response.Messages.First(), results.Response.Messages.Last());
				data.Messages = results.Response.Messages;
			}

			// update last-result data
			_lastResult = data;

			if (results.Response != null && results.Response.Messages != null)
			{
				// update messages
				foreach (GroupMeMessageData message in results.Response.Messages)
				{
					_loadedMessages.Add(message.MessageId, message);
				}
			}

			return data;
		}

		private async Task<GroupMeApiResponse<MessagesIndexData>> LoadResultsForCommand(
			IWebApiCommand<GroupMeApiResponse<MessagesIndexData>> command)
		{
			GroupMeApiResponse<MessagesIndexData> results = await _webApiAccess.ExecuteCommand(command);

			if (results.Meta.ParsedCode != GroupMeApiResultCode.Ok && results.Meta.ParsedCode != GroupMeApiResultCode.NotModified)
			{
				throw new GroupMeException(results.Meta);
			}

			return results;
		}

		/// <summary>
		/// Resets the last-result, oldest- and newest message and clears all loaded messages if the direction is set to current.
		/// </summary>
		/// <param name="direction">The direction to check.</param>
		/// <returns>True if the data was reset, false if not.</returns>
		private bool ResetDataIfDirectionIsCurrent(MessageLoadDirection direction)
		{
			if (direction == MessageLoadDirection.Current)
			{
				_lastResult = null;
				OldestMessageId = string.Empty;
				NewestMessageId = string.Empty;
				_loadedMessages.Clear();

				return true;
			}

			return false;
		}

		private void SetMessageIdBoundariesForDirection(MessageLoadDirection direction, GroupMeMessageData firstNewMessage,
			GroupMeMessageData lastNewMessage)
		{
			switch (direction)
			{
				case MessageLoadDirection.Current:
					NewestMessageId = firstNewMessage.MessageId;
					OldestMessageId = lastNewMessage.MessageId;
					break;
				case MessageLoadDirection.Backward:
					OldestMessageId = lastNewMessage.MessageId;
					break;
				case MessageLoadDirection.Forward:
					NewestMessageId = firstNewMessage.MessageId;
					break;
			}
		}
	}
}